package com.russell.util;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Date Utility Class. This is used to convert Strings to Dates and Timestamps. Utility also contains common date
 * methods.
 * 
 * 
 * 
 * 
 * @author <a href="mailto:fvinluan@gmail.com">Francis Vinluan</a>
 */
public final class DateUtil {
    private final static Logger logger = LoggerFactory.getLogger(DateUtil.class);


    /**
     * Adds to a date returning a new object. The original date object is unchanged.
     * 
     * @param date
     *            the date, not null
     * @param calendarField
     *            the calendar field to add to
     * @param amount
     *            the amount to add, may be negative
     * @return the new date object with the amount added
     * @throws IllegalArgumentException
     *             if the date is null
     * 
     */
    private static Date add(Date date, int calendarField, int amount) {
        if (date == null) {
            throw new IllegalArgumentException("The date must not be null");
        }
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        c.add(calendarField, amount);
        return c.getTime();
    }

    /**
     * Adds a number of days to a date returning a new object. The original date object is unchanged.
     * 
     * @param date
     *            the date, not null
     * @param amount
     *            the amount to add, may be negative
     * @return the new date object with the amount added
     * @throws IllegalArgumentException
     *             if the date is null
     */
    public static Date addDays(Date date, int amount) {
        return add(date, Calendar.DAY_OF_MONTH, amount);
    }

    /**
     * Adds a number of hours to a date returning a new object. The original date object is unchanged.
     * 
     * @param date
     *            the date, not null
     * @param amount
     *            the amount to add, may be negative
     * @return the new date object with the amount added
     * @throws IllegalArgumentException
     *             if the date is null
     */
    public static Date addHours(Date date, int amount) {
        return add(date, Calendar.HOUR_OF_DAY, amount);
    }

    /**
     * Adds a number of months to a date returning a new object. The original date object is unchanged.
     * 
     * @param date
     *            the date, not null
     * @param amount
     *            the amount to add, may be negative
     * @return the new date object with the amount added
     * @throws IllegalArgumentException
     *             if the date is null
     */
    public static Date addMonths(Date date, int amount) {
        return add(date, Calendar.MONTH, amount);
    }

    /**
     * Adds a number of seconds to a date returning a new object. The original date object is unchanged.
     * 
     * @param date
     *            the date, not null
     * @param amount
     *            the amount to add, may be negative
     * @return the new date object with the amount added
     * @throws IllegalArgumentException
     *             if the date is null
     */
    public static Date addSeconds(Date date, int amount) {
        return add(date, Calendar.SECOND, amount);
    }

    /**
     * Adds a number of weeks to a date returning a new object. The original date object is unchanged.
     * 
     * @param date
     *            the date, not null
     * @param amount
     *            the amount to add, may be negative
     * @return the new date object with the amount added
     * @throws IllegalArgumentException
     *             if the date is null
     */
    public static Date addWeeks(Date date, int amount) {
        return add(date, Calendar.WEEK_OF_YEAR, amount);
    }

    /**
     * Returns the Date equivalent of the GMT Date Time parameter.
     * 
     * @param strDate
     *            String representation of the Date being processed. This should
     * @param aMask
     *            String format of the date
     * @return
     * @throws ParseException
     */
    public static Date convertGMTDateStringToDate(String strDate, String aMask) throws ParseException {

        SimpleDateFormat format = new SimpleDateFormat(aMask);
        format.setTimeZone(TimeZone.getTimeZone("GMT"));
        return format.parse(strDate);
    }

    /**
     * This method generates a string representation of a date/time in the format you specify on input.
     * 
     * @param dateString
     * @param pattern
     * @return
     * @throws ParseException
     */
    public static final Date convertStringToDate(String dateString, String pattern) throws ParseException {
        if (StringUtils.isBlank(dateString)) {
            return null;
        }

        Date date = null;
        DateFormat df = new SimpleDateFormat(pattern);

        if (logger.isDebugEnabled()) {
            logger.debug("converting '" + dateString + "' to date with mask '" + pattern + "'");
        }

        try {
            date = df.parse(dateString);
        } catch (ParseException pe) {
            // logger.error("ParseException: " + pe);
            throw new ParseException(pe.getMessage(), pe.getErrorOffset());
        }

        return (date);
    }

    /**
     * Returns a new instance of the date passed. Mainly used to make sure that date will not mutate
     * 
     * @param date
     *            date which the new date will be based on
     * @return a converted Date object
     */
    public static Date copyDate(Date date) {

        if (date == null) {
            return null;
        }

        return new Date(date.getTime());
    }

    /**
     * Calculates the difference in days of the given dates.
     * 
     * @param startDate
     * @param endDate
     * @return int
     */
    public static long dayDifference(Date startDate, Date endDate) {

        Date tmpStartDate = DateUtils.truncate(startDate, Calendar.DAY_OF_MONTH);
        Date tmpEndDate = DateUtils.truncate(endDate, Calendar.DAY_OF_MONTH);

        GregorianCalendar startCalendar = new GregorianCalendar();
        startCalendar.setTime(tmpStartDate);

        GregorianCalendar endCalendar = new GregorianCalendar();
        endCalendar.setTime(tmpEndDate);

        long milliseconds1 = startCalendar.getTimeInMillis();
        long milliseconds2 = endCalendar.getTimeInMillis();
        long diff = milliseconds2 - milliseconds1;
        long diffMinutes = diff / (1000 * 60 * 60 * 24);

        return diffMinutes;

    }

    /**
     * Calculates the difference in minutes of the given dates.
     * 
     * @param startDate
     * @param endDate
     * @return long
     */
    public static long minuteDifference(Date startDate, Date endDate) {

        GregorianCalendar startCalendar = new GregorianCalendar();
        startCalendar.setTime(startDate);

        GregorianCalendar endCalendar = new GregorianCalendar();
        endCalendar.setTime(endDate);

        long milliseconds1 = startCalendar.getTimeInMillis();
        long milliseconds2 = endCalendar.getTimeInMillis();
        long diff = milliseconds2 - milliseconds1;

        long diffMinutes = diff / (60 * 1000);

        return diffMinutes;
    }

    /**
     * Calculates the difference in hours of the given dates.
     * 
     * @param startDate
     * @param endDate
     * @return long
     */
    public static long hourDifference(Date startDate, Date endDate) {

        GregorianCalendar startCalendar = new GregorianCalendar();
        startCalendar.setTime(startDate);

        GregorianCalendar endCalendar = new GregorianCalendar();
        endCalendar.setTime(endDate);
        long milliseconds1 = startCalendar.getTimeInMillis();
        long milliseconds2 = endCalendar.getTimeInMillis();
        long diff = milliseconds2 - milliseconds1;
        long diffHours = diff / (60 * 60 * 1000);

        return diffHours;
    }

    /**
     * This method is a wrapper on DateFormatUtils.format() since it doesn't take care of null values.
     * 
     * @param date
     *            date from database as a string
     * @return formatted string for the ui
     */
    public static final String format(Date date, String pattern) {

        String returnValue = (date == null) ? "" : DateFormatUtils.format(date, pattern);
        return returnValue;
    }

    /**
     * This method to determine the number of days between two dates excluding Saturday and Sunday.
     * 
     * @param date
     *            date to check
     * @return boolean
     */
    public static int getDateDifferenceInDaysExcludingWeekend(Date fromDate, Date toDate) {
        if (fromDate == null || toDate == null) {
            return 0;
        }

        if (fromDate.after(toDate)) {
            Date temp = fromDate;
            fromDate = toDate;
            toDate = temp;
        }

        Calendar calFrom = Calendar.getInstance();
        calFrom.setTime(fromDate);
        Calendar calTo = Calendar.getInstance();
        calTo.setTime(toDate);
        int iNoOfWorkingDays = 0;
        do {
            if (calFrom.get(Calendar.DAY_OF_WEEK) != Calendar.SATURDAY
                    && calFrom.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) {
                iNoOfWorkingDays += 1;
            }
            calFrom.add(Calendar.DATE, 1);
        } while (calFrom.getTimeInMillis() < calTo.getTimeInMillis());
        return iNoOfWorkingDays;
    }

    /**
     * This method generates a string representation of a date's date/time in the format you specify on input.
     * 
     * @param aMask
     *            the date pattern the string is in
     * @param aDate
     *            a date object
     * @return a formatted string representation of the date
     * @see java.text.SimpleDateFormat
     */
    public static final String getDateTime(String aMask, Date aDate) {
        SimpleDateFormat df = null;
        String returnValue = "";

        if (aDate == null) {
            logger.error("date is null!");
        } else {
            df = new SimpleDateFormat(aMask);
            returnValue = df.format(aDate);
        }

        return (returnValue);
    }

    /**
     * Returns the new date object the system is using.
     * 
     * @return Date
     */
    public static Date getSystemDate() {
        return new Date();
    }

    // /**
    // * This method returns the current date time in the format: MM/dd/yyyy
    // * HH:MM. a
    // *
    // * @param theTime
    // * the current time
    // * @return the current date/time
    // */
    // public static String getTimeNow(Date theTime) {
    // return getDateTime(timePattern, theTime);
    // }

    /**
     * This method returns the current date in the format: MM/dd/yyyy
     * 
     * @return the current date
     * @throws ParseException
     */
    public static Calendar getToday() throws ParseException {
        Date today = new Date();
        SimpleDateFormat df = new SimpleDateFormat("MM/dd/yyyy");

        // This seems like quite a hack (date -> string -> date),
        // but it works ;-)
        String todayAsString = df.format(today);
        Calendar cal = new GregorianCalendar();
        cal.setTime(convertStringToDate(todayAsString, "MM/dd/yyyy"));
        return cal;
    }

    /**
     * This method to determine if the date is a weekend.
     * 
     * @param date
     *            the date to check
     * @return boolean
     */
    public static boolean isWeekend(Date date) {
        Calendar cal = new GregorianCalendar();
        cal.setTime(date);

        if (cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY || cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) {
            return true;
        }
        return false;
    }

    /**
     * Calculates the difference in weeks of the given dates.
     * 
     * @param startDate
     * @param endDate
     * @return int
     */
    public static int weekDifference(Date startDate, Date endDate) {

        GregorianCalendar startCalendar = new GregorianCalendar();
        startCalendar.setTime(startDate);

        GregorianCalendar endCalendar = new GregorianCalendar();
        endCalendar.setTime(endDate);

        int weekCount = 0;
        while (startCalendar.equals(endCalendar) || startCalendar.before(endCalendar)) {
            startCalendar.add(Calendar.WEEK_OF_YEAR, 1);
            weekCount++;
        }

        return weekCount;
    }

    /**
     * Gets the first date of any year.
     * 
     * @param addToCurrentYear
     *            can be negative. Gets the first day of the current year if zero is passed
     * 
     * @return java.util.Date
     */
    public static Date getFirstDayOfYear(int addToCurrentYear) {
        Calendar firstDayOfYear = Calendar.getInstance();

        firstDayOfYear.set(Calendar.DAY_OF_YEAR, 1);
        firstDayOfYear.add(Calendar.YEAR, addToCurrentYear);

        return firstDayOfYear.getTime();
    }

    /**
     * Gets the last date of any year.
     * 
     * @param addToCurrentYear
     *            can be negative. Gets the last day of the current year if zero is passed
     * 
     * @return java.util.Date
     */
    public static Date getLastDayOfYear(int addToCurrentYear) {
        Calendar lastDayOfYear = Calendar.getInstance();

        lastDayOfYear.add(Calendar.YEAR, addToCurrentYear);
        lastDayOfYear.set(Calendar.MONTH, Calendar.DECEMBER);
        lastDayOfYear.set(Calendar.DAY_OF_MONTH, 31);

        return lastDayOfYear.getTime();
    }

    /**
     * Gets the first possible date of the month based on date passed
     * 
     * @param date
     * @return
     */
    public static Date getFirstDateOfMonth(Date date) {
        if (date != null) {
            Date tmpDate = DateUtils.truncate(date, Calendar.DAY_OF_MONTH);
            Calendar cal = Calendar.getInstance();
            cal.setTime(tmpDate);
            cal.set(Calendar.DAY_OF_MONTH, 1);

            return cal.getTime();
        }
        return null;

    }

    public static Date getFirstDateOfWeek(Date date) {
        if (date != null) {
            Calendar cal = Calendar.getInstance();
            cal.setTime(date);
            cal.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
            return cal.getTime();
        }
        return null;

    }

    public static Date getLastDateOfWeek(Date date) {
        if (date != null) {
            Calendar cal = Calendar.getInstance();
            cal.setTime(date);
            cal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
            return cal.getTime();
        }
        return null;
    }

    /**
     * Gets the first possible date of the month based on date passed
     * 
     * @param date
     * @return
     */
    public static Date getLastDateOfDay(Date date) {
        Date tmpDate = DateUtils.truncate(date, Calendar.DAY_OF_MONTH);
        Calendar cal = Calendar.getInstance();
        cal.setTime(tmpDate);
        cal.add(Calendar.DAY_OF_YEAR, 1);
        cal.add(Calendar.MILLISECOND, -1);

        return cal.getTime();
    }

    /**
     * Gets the last possible date of the month based on date passed
     * 
     * @param date
     * @return
     */
    public static Date getLastDateOfMonth(Date date) {
        if (date != null) {
            Date tmpDate = DateUtils.truncate(date, Calendar.DAY_OF_MONTH);
            Calendar cal = Calendar.getInstance();
            cal.setTime(tmpDate);
            cal.set(Calendar.DAY_OF_MONTH, 1);
            cal.add(Calendar.MONTH, 1);
            cal.add(Calendar.MILLISECOND, -1);
            return cal.getTime();
        }
        return null;
    }

    /**
     * Gets midnight of the day in which date lies
     * 
     * @param date
     * @return
     */
    public static Date getDateWithStartTimeOfTheDay(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);

        return cal.getTime();
    }

    /**
     * Gets midnight of the day in which date lies
     * 
     * @param date
     * @return
     */
    public static Date getDateWithEndTimeOfTheDay(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.HOUR_OF_DAY, 23);
        cal.set(Calendar.MINUTE, 59);
        cal.set(Calendar.SECOND, 59);

        return cal.getTime();
    }

    /**
     * Gets the first possible date of the month based on the year and month passed
     * 
     * @param year
     * @param month
     * @return
     */
    public static Date getFirstDateOfMonth(int year, int month) {
        Calendar date = new GregorianCalendar(year, month, 1);

        return date.getTime();
    }

    /**
     * Gets the last possible date of the month based on the year and month passed
     * 
     * @param year
     * @param month
     * @return
     */
    public static Date getLastDateOfMonth(int year, int month) {
        Calendar date = new GregorianCalendar(year, month, 1);
        date.add(Calendar.MONTH, 1);
        date.add(Calendar.MILLISECOND, -1);

        return date.getTime();
    }

    /**
     * Gets the first day of the current financial year.
     * 
     * @param addToCurrentYear
     *            can be negative. Gets the first day of the current financial year if zero is passed
     * 
     * @return the first day of the current financial year
     */
    public static Date getFirstDayOfFinancialYear(int addToCurrentYear) {
        Calendar firstDayOfYear = Calendar.getInstance();

        firstDayOfYear.set(Calendar.DAY_OF_MONTH, 1);
        firstDayOfYear.set(Calendar.MONTH, Calendar.DECEMBER);
        firstDayOfYear.add(Calendar.YEAR, addToCurrentYear - 1);
        firstDayOfYear.set(Calendar.HOUR_OF_DAY, 0);
        firstDayOfYear.set(Calendar.MINUTE, 0);
        firstDayOfYear.set(Calendar.SECOND, 0);

        return firstDayOfYear.getTime();
    }

    /**
     * Gets the last day of the current financial year.
     * 
     * @param addToCurrentYear
     *            can be negative. Gets the last day of the current financial year if zero is passed
     * 
     * @return the last day of the current financial year
     */
    public static Date getLastDayOfFinancialYear(int addToCurrentYear) {
        Calendar lastDayOfYear = Calendar.getInstance();

        lastDayOfYear.set(Calendar.DAY_OF_MONTH, 30);
        lastDayOfYear.set(Calendar.MONTH, Calendar.NOVEMBER);
        lastDayOfYear.add(Calendar.YEAR, addToCurrentYear);
        lastDayOfYear.set(Calendar.HOUR_OF_DAY, 23);
        lastDayOfYear.set(Calendar.MINUTE, 23);
        lastDayOfYear.set(Calendar.SECOND, 59);

        return lastDayOfYear.getTime();
    }

    /**
     * 
     * @param date1
     * @param date2
     * @return
     */
    public static boolean isSameDay(Date date1, Date date2) {
        if (date1 == null || date2 == null) {
            if (date1 == null && date2 == null) {
                return true;
            } else {
                return false;
            }
        } else {
            return DateUtils.isSameDay(date1, date2);
        }

    }

    private DateUtil() {
        super();
    }
}
